package com.zzw.junit5demo.extension;

import org.junit.jupiter.api.extension.*;
import org.junit.platform.commons.support.AnnotationSupport;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.internal.configuration.MockAnnotationProcessor;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.session.MockitoSessionLoggerAdapter;
import org.mockito.quality.Strictness;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Parameter;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;


/**
 * @author zzw
 * @version 1.0
 * @title MockitoExtension
 * @description <description class purpose>
 * @datetime 2023-06-15 23:10
 */
public class MockitoExtension implements TestInstancePostProcessor, BeforeEachCallback, AfterEachCallback, ParameterResolver {

    private final static ExtensionContext.Namespace MOCKITO = ExtensionContext.Namespace.create("org.mockito");

    private final static String SESSION = "session";
    private final static String TEST_INSTANCE = "testInstance";

    private final Strictness strictness;

   // 此构造函数由JUnit Jupiter通过反射或ServiceLoader调用时
    @SuppressWarnings("unused")
    public MockitoExtension() {
        this(Strictness.STRICT_STUBS);
    }

    private MockitoExtension(Strictness strictness) {
        this.strictness = strictness;
    }

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MockitoSettings {

        /**
         * 配置此测试中使用的严格性
         * @return The strictness to configure, by default {@link Strictness#STRICT_STUBS}
         */
        Strictness strictness() default Strictness.STRICT_STUBS;
    }


    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        context.getStore(MOCKITO).remove(SESSION, MockitoSession.class).finishMocking();
    }

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {

        Set<Object> testInstances = new LinkedHashSet<>();
        testInstances.add(context.getRequiredTestInstance());

        this.collectParentTestInstances(context, testInstances);

        Strictness actualStrictness = this.retrieveAnnotationFromTestClasses(context)
                .map(MockitoSettings::strictness)
                .orElse(strictness);

        MockitoSession session = Mockito.mockitoSession()
                .initMocks(testInstances.toArray())
                .strictness(actualStrictness)
                .logger(new MockitoSessionLoggerAdapter(Plugins.getMockitoLogger()))
                .startMocking();

        context.getStore(MOCKITO).put(SESSION, session);
    }

    private Optional<MockitoSettings> retrieveAnnotationFromTestClasses(final ExtensionContext context) {
        ExtensionContext currentContext = context;
        Optional<MockitoSettings> annotation;

        do {
            annotation = AnnotationSupport.findAnnotation(currentContext.getElement(), MockitoSettings.class);

            if (!currentContext.getParent().isPresent()) {
                break;
            }

            currentContext = currentContext.getParent().get();
        } while (!annotation.isPresent() && currentContext != context.getRoot());

        return annotation;
    }

    private void collectParentTestInstances(ExtensionContext context, Set<Object> testInstances) {
        Optional<ExtensionContext> parent = context.getParent();

        while (parent.isPresent() && parent.get() != context.getRoot()) {
            ExtensionContext parentContext = parent.get();

            Object testInstance = parentContext.getStore(MOCKITO).remove(TEST_INSTANCE);

            if (testInstance != null) {
                testInstances.add(testInstance);
            }

            parent = parentContext.getParent();
        }
    }

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterContext.isAnnotated(Mock.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
        final Parameter parameter = parameterContext.getParameter();
        Mock mockMock = parameterContext.findAnnotation(Mock.class).get();
        return MockAnnotationProcessor.processAnnotationForMock(
                mockMock, parameter.getType(), parameter::getParameterizedType, parameter.getName());
    }

    @Override
    public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
        context.getStore(MOCKITO).put(TEST_INSTANCE, testInstance);
    }
}
